# frozen_string_literal: true

module BinPacking
  class Packer2d
    # FIRST_MAX_SIDE = ->(a, b) {b[:length] <=> a[:length]}
    # FIRST_MAX_AREA = ->(a, b) {}
    def initialize; end

    def test
      container = {dimensions: [40, 50]}
      items = [
        {dimensions: [3, 4], count: 4, turn: true},
        {dimensions: [2, 4], count: 5, turn: true},
        {dimensions: [3, 3], count: 3, turn: true},
        {dimensions: [4, 4], count: 2, turn: true},
        {dimensions: [5, 1], count: 6, turn: true},
      ]

      pack(container: container, items: items)
    end

    # rubocop:disable Metrics/AbcSize
    def pack(container:, items:)
      adjust_rectangle(container)
      items.each {|item| adjust_rectangle(item)}

      packing = init_pack_result(container)
      area_list, perimeter_list = calc_items_area_n_perimeter(items)

      items.each_with_index do |item, items_index|
        remain_limitation = {
          area: area_list[items_index.next..-1].min || 0,
          perimeter: perimeter_list[items_index.next..-1].min || 0,
        }

        remain_limitation_with_current = {
          area: [remain_limitation[:area], item[:area]].min,
          perimeter: [remain_limitation[:perimeter], item[:perimeter]].min,
        }

        item_count = item[:count]
        item_count.times do |item_index|
          items_limitation = item_index < item_count.pred ? remain_limitation_with_current : remain_limitation

          raise 'Unpacking item' unless pack_single_item(packing, item, items_limitation)
        end
      end

      packing
    end
    # rubocop:enable Metrics/AbcSize

    private

    def calc_items_area_n_perimeter(items)
      items.reduce([[], []]) do |(area_temp, perimeter_temp), item|
        area_temp << item[:area]
        perimeter_temp << item[:perimeter]

        [area_temp, perimeter_temp]
      end
    end

    def pack_single_item(packing, item, items_limitation)
      min_area = items_limitation[:area]
      min_perimeter = items_limitation[:perimeter]

      packing[:spaces].each_with_index do |space, space_index|
        next unless (placement = try_place(space, item))

        place(packing, space_index, placement, min_area, min_perimeter)
        return true
      end

      false
    end

    def init_pack_result(container)
      {
        container: container,
        placements: [],
        use_rate: 0.0,
        spaces: [{
          dimensions: container[:dimensions],
          position: [0, 0],
          area: container[:dimensions].reduce(&:*),
          perimeter: container[:dimensions].sum * 2,
        }],
        small_spaces: [],
      }
    end

    def place(packing, space_index, placement, min_area, min_perimeter)
      new_spaces = break_up(packing[:spaces][space_index], placement)

      active_spaces = []
      new_spaces.each do |new_space|
        if new_space[:area] < min_area || new_space[:perimeter] < min_perimeter
          packing[:small_spaces] << new_space
        else
          active_spaces << new_space
        end
      end

      # TODO: left -> right
      packing[:spaces][space_index..space_index] = active_spaces
    end

    def break_up(space, placement)
      # TODO: left -> right
      sl, sw = space[:dimensions]
      pl, pw = placement[:dimensions]
      x0, y0 = space[:position]

      [
        {
          dimensions: [pl, sw - pw],
          position: [x0, y0 + sw],
          area: pl * (sw - pw),
          perimeter: (pl + sw - pw) * 2,
        },
        {
          dimensions: [sl - pl, sw],
          position: [x0 + pl, y0],
          area: (sl - pl) * sw,
          perimeter: (sl - pl + sw) * 2,
        },
      ]
    end

    def try_place(space, item)
      space_length, space_width = space[:dimensions]
      item_length, item_width = item[:dimensions]
      [[item_length, item_width], [item_width, item_length]].each do |length, width|
        next if space_length < length || space_width < width

        return {
          dimensions: [length, width],
          position: space[:position],
        }
      end

      nil
    end

    def adjust_rectangle(rectangle)
      rectangle[:dimensions].sort! {|a, b| b <=> a}

      rectangle.merge!(
        area: rectangle[:dimensions].reduce(&:*),
        perimeter: rectangle[:dimensions].sum * 2,
      )
    end
  end
end
